#include <malloc.h>
#include <memory.h>
#include "bus.h"
#include "type.h"
#include "ppu.h"
#include "util.h"

#define STACK_BASELINE (0x100)

static inline uint16 ines_bus_map(uint16 addr) {
    if (addr >= 0x800 && addr <= 0x1FFF) {
        addr &= 0x07FF;
    } else if (addr >= 0x2008 && addr <= 0x3FFF) {
        addr &= 0x2007;
    }
    return addr;
}

extern MemBus *ines_bus_new() {
    MemBus *bus = malloc(sizeof(MemBus));
    bus->sram = ines_new_array(0x2000);
    bus->ram = ines_new_array(0x800);
    bus->expansion = ines_new_array(0x1FE0);
    return bus;
}

extern void ines_bus_dispose(MemBus *bus) {
    if (bus == NULL) {
        return;
    }
    free(bus->ram);
    free(bus->sram);
    free(bus->expansion);
    free(bus);
}

extern void ines_bus_sram_fill(MemBus *bus, const uint8 *buf, int offset, int size) {
    for (int i = 0; i < size; ++i) {
        bus->sram[offset + i] = buf[i];
    }
}

extern uint8 ines_bus_read(NesConsole *console, uint16 addr) {
    uint8 b = 0;
    addr = ines_bus_map(addr);
    MemBus *bus = console->bus;
    if (addr < 0x2000) {
        b = bus->ram[addr];
    } else if (addr == 0x2002) {
        b = ines_ppu_status(console);
    } else if (addr == 0x2004) {
        b = ines_ppu_oam_read(console);
    } else if (addr == 0x2007) {
        b = ines_ppu_read(console);
    } else if (addr >= 0x4020 && addr < 0x6000) {
        b = bus->expansion[addr - 0x4020];
    } else if (addr >= 0x6000 && addr < 0x8000) {
        b = bus->sram[addr - 0x6000];
    } else if (addr >= 0x8000) {
        b = console->cartridge->ines_prg_read(console, addr);
    }
    return b;
}

extern uint16 ines_bus_read_uint16(NesConsole *console, uint16 addr) {
    uint8 lsb = ines_bus_read(console, addr);
    uint8 msb = ines_bus_read(console, addr + 1);
    return lsb | msb << 8;
}

extern void ines_bus_stack_push(NesConsole *console, uint8 b) {
    CPU *cpu = console->cpu;
    uint8 s = cpu->s;
    MemBus *bus = console->bus;
    bus->ram[STACK_BASELINE + s--] = b;
    cpu->s = s;
}

extern void ines_bus_stack_push0(NesConsole *console, uint16 b) {
    uint8 lsb = b & 0xFF;
    uint8 msb = (b >> 8) & 0xFF;
    ines_bus_stack_push(console, msb);
    ines_bus_stack_push(console, lsb);
}

extern inline uint8 ines_bus_stack_pop(NesConsole *console) {
    CPU *cpu = console->cpu;
    uint8 s = cpu->s;
    MemBus *bus = console->bus;
    uint8 b = bus->ram[STACK_BASELINE + (++s)];
    cpu->s = s;
    return b;
}

extern uint16 ines_bus_stack_pop0(NesConsole *console) {
    uint8 lsb = ines_bus_stack_pop(console);
    uint8 msb = ines_bus_stack_pop(console);
    return lsb | msb << 8;
}

extern void ines_bus_write(NesConsole *console, uint16 addr, uint8 b) {
    addr = ines_bus_map(addr);
    MemBus *bus = console->bus;
    if (addr < 0x2000) {
        bus->ram[addr] = b;
    } else if (addr == 0x2000) {
        ines_ppu_control(console, b);
    } else if (addr == 0x2001) {
        ines_ppu_mask(console, b);
    } else if (addr == 0x2004) {
        ines_ppu_oam_write(console, b);
    } else if (addr == 0x2005) {
        ines_ppu_scroll(console, b);
    } else if (addr == 0x2006) {
        ines_ppu_addr(console, b);
    } else if (addr == 0x4014) {
        ines_ppu_dma(console, b);
    } else if (addr == 0x2007) {
        ines_ppu_write(console, b);
    }
}

extern int ines_bus_read_utf8char(NesConsole *console, uint8 *arr, int length) {
    CPU *cpu = console->cpu;
    uint16 index = cpu->pc;
    CartridgeMapper *mapper = console->cartridge;
    int i = 0;
    for (; i < length; i++) {
        uint8 b = mapper->ines_prg_read(console, index);
        arr[i] = b;
        if (b == '\0') {
            break;
        }
        index++;
    }
    return i;
}